Move the scraper off Playwright to httpx + containerize for the k8s CronJob - #44
Merged
Conversation
Egress-IP-first scaffolding to move the scraper off GitHub Actions (blocked by the Fastly bot-challenge, #28) onto the workloads EKS cluster, sharing bitergia's static NAT egress IP for the Mozilla allowlist ask (#27). Plain-HTTP image, CSVs stay in git, fine-grained PAT for pushes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
aatchison
marked this pull request as draft
July 13, 2026 17:40
Two-phase, TDD where testable: Phase A (this repo) swaps sumo.py's Playwright engine for httpx behind the frozen SumoBrowser API, packages a browser-free arm64 image whose entrypoint clones/refreshes/commits, and builds it in CI; Phase B (platform-infrastructure) adds the Pulumi ECR+OIDC, the deploy manifests, and the ArgoCD app. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The sandbox default is Python 3.14, against which playwright's greenlet dependency has no wheel and fails to build, breaking `uv sync`/`uv run`. Pin to 3.12 (within requires-python >=3.10) so the mandated `uv run pytest` works. (greenlet/playwright are removed in the next task, but the pin keeps the toolchain reproducible regardless of the host default.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The CronJob commits the high-water mark to the repo each active run instead of the (unavailable) GitHub Actions cache. run_refresh.py's 26h lookback covers its initial absence, so no seed file is needed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pass the workflow_dispatch tag input via an env var instead of interpolating github.event.* into the run script, and validate its charset. Flagged HIGH (ci-cd-script-injection) by review; the job holds ECR-push OIDC creds. Behavior unchanged for the default git-<sha> path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ss, read-only-FS workdir)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…oint verification Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Collaborator
|
looks like a great spec to me @aatchison , love to have this working before end of july please and thank-you :-), I know e are all busy :-) so if not we have my Chrome and Firefox extension as a fallback cc / @lisajill |
The workloads cluster's NAT egress IPs are already allowlisted by Mozilla; the earlier "still blocked" conclusion came from testing on a workstation rather than in the cluster. Verified 2026-07-27 from a pod in each AZ with the scraper's own httpx client: 3.67.52.124 and 63.182.70.185 both return 200 + JSON, while the same request from a non-allowlisted network returns the challenge HTML. So the CronJob works as soon as it is deployed -- the remaining gate is mechanical (image build, PAT secret, unsuspend), not an upstream ask. Also records that the read-only-root-FS question is resolved: the real image runs the full clone/refresh/commit flow with uv reusing the baked venv, given the four UV_* env vars now set in the CronJob. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
aatchison
marked this pull request as ready for review
July 27, 2026 17:55
…ema-check edit Brings the branch up to date with main (66 commits, incl. the browser extension and its imported data), and makes this PR strictly non-destructive: - pyproject.toml: playwright moves to an OPTIONAL dependency group rather than being removed outright, so poc.py and any manual browser experiment still work via `uv sync --group playwright`. It is not installed by default and is absent from the container image (verified). - .github/workflows/schema-check.yml: reverted to main. The earlier "park the daily cron" commit was based on a false premise -- all three workflows are already disabled_manually at the GitHub level, so the edit changed nothing and its rationale (stop spurious api-blocked issues) was wrong; a disabled workflow opens none. - .gitignore: keep main's extension rules AND the .refresh-hwm un-ignore, with a note explaining why the latter is deliberate. - Docs (CLAUDE.md, README, spec, plan, poc.py): drop the claim that scrape.yml is the live production refresh -- it is disabled and last ran 2026-07-10. Record that the extension is what currently produces data, that its output is format-compatible with run_refresh.py (import_json.py reuses the scrapers' writers), and flag the open decision: whether the CronJob replaces the extension or runs beside it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The k8s CronJob needs to run alongside the browser extension without both committing the same day-CSVs. Three small, default-off knobs: - AAQ_DATA_ROOT prefixes the <year>/ tree in default_output_path(). Unset (the default) keeps the exact committed layout, so nothing changes for existing runs. - run_refresh.py now derives the questions path from that same helper instead of formatting it a second time, so the path can only be built one way. - entrypoint.sh takes GIT_ADD_PATHS (default `20*/ .refresh-hwm`) and stages each path independently, tolerating ones that don't exist. A deployment writing under a data root points this at that directory, so the committed CSVs cannot be staged even by accident. Also fixes a real fragility the new isolation test exposed: `git pull --rebase` refuses to run with a dirty working tree, so any unstaged file left in the clone would wedge the push-retry loop for all five attempts and fail an otherwise-good run. Now uses --autostash. Tests: default layout unchanged; AAQ_DATA_ROOT relocates; and an end-to-end entrypoint test asserting that with GIT_ADD_PATHS set only the shakedown directory is committed while a concurrently-modified real CSV is left alone. 14/14 passing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase A of moving the AAQ scraper onto a Kubernetes CronJob. Pairs with platform-infrastructure#802 (manifests, ArgoCD app, Pulumi). Relates to #27, #28.
This PR is additive — it removes no working capability. It does not touch
extension/,import_json.py, the CSV writers, or any workflow other than adding a new one.Why
support.mozilla.orgis behind a Fastly bot-challenge that fingerprints automated browsers and non-allowlisted source IPs, so the Playwright approach is dead (#28) and Actions runners can't reach the API.The workloads EKS cluster's NAT egress IPs are already allowlisted — verified 2026-07-27 from a pod in each AZ using this branch's exact httpx client:
3.67.52.124and63.182.70.185both return HTTP 200 + JSON, while the identical request from a non-allowlisted network returns the challenge HTML.What's here
sumo.py: Playwright → httpx. TheSumoBrowserpublic API is frozen — class name, all__init__kwargs, and thefetch_json/paginatemethod bodies are byte-identical. Only__init__/__enter__/__exit__/_raw_fetchchanged, so no consumer needed edits, and the retry/deferral/challenge contract (ChallengeError,RateLimitDeferral, exit 75) is preserved.uv sync --group playwrightstill runspoc.py. Not installed by default; verified absent from the image.Dockerfile— browser-freepython:3.12-slim, arm64, nonroot.deploy/entrypoint.sh— clone →run_refresh.py→ commit CSVs +.refresh-hwm→ push with rebase-retry. PAT supplied via a git credential helper, never in argv or a URL..refresh-hwmtracked in git (the pod is stateless; the repo is the state).aaq-scraper-image.yml— arm64 build → shared ECR via OIDC.docs/superpowers/.Relationship to the browser extension
mainmoved on while this was in flight, and the extension is what currently produces data (imported through 2026-07-26); all three GitHub workflows aredisabled_manually. This branch is merged up to date with that work and leaves it untouched.Usefully,
import_json.pyreuses the scrapers' own writers (build_fieldnames,flatten_answer,COLUMNS), so extension-imported andrun_refresh.py-generated CSVs are format-compatible — no churn if both run.Open decision, deliberately not made here: whether the CronJob replaces the extension or runs beside it. Both would commit the same day-CSVs on
main. The CronJob ships suspended, so this is decided at cutover, not at merge.Testing
11/11 passing, plus container-level verification:
tests/test_fetch_json.py— characterization tests stubbing only the transport, so they pin the retry/deferral/challenge semantics and passed against the old Playwright code before the swap and after it. PlusMockTransporttests for the httpx→dict mapping.tests/test_entrypoint.py— integration against a local bare git repo (no network/token): new CSV → one commit; unchanged run → none; and a competing push forces the rebase-retry path, asserting both commits survive.docker run --read-only: full clone → refresh → commit works, anduv runreuses the baked venv (sys.prefix=/app/.venv, no PyPI fetch) via the fourUV_*vars now in the CronJob spec.Review notes
Issues found and fixed during review:
ps) → replaced with a credential helper.${{ github.event.inputs.tag }}into arun:block — script injection in a job holding ECR-push creds → passed viaenv+ charset validation.schema-check.ymlwas reverted — the workflow is already disabled at the GitHub level, so it changed nothing and its rationale was false.Merge #802 first — it creates the OIDC role this repo's image workflow assumes.
🤖 Generated with Claude Code